[알고쓰자] Spring - 01 : Spring 의 시작

[알고쓰자] Spring - 01 : Spring 의 시작

Intro

백엔드 개발자에게 Spring Framework 란 어떤 의미일까요? 누군가는 Spring Framework 를 통해 웹 개발에 입문했을 수도 있고, 또 다른 누군가에겐 그저 수 많은 도구 중에 선택지일 뿐일 수도 있겠죠. 여러 설문조사들을 살펴보면 Spring Boot, Django, Express.js, Ruby on Rails, Laravel, Asp.net 등이 백엔드 애플리케이션 개발에 주로 쓰입니다. 독자분들이 가장 익숙한 프레임워크는 무엇인가요?

개인적으로는 Spring Framework, Express.js 정도인 것 같아요. 프레임워크는 그저 도구일 뿐이라 생각하는 편이라 무엇이 더 낫다고 판단하는 편은 아니에요. 가장 중요한 건 팀원들과 얼마나 시너지를 낼 수 있느냐 라고 생각해요. 서비스의 규모나 성격에 따라 달라지는 문제라고 생각하지만, 국내에서는 유독 Spring Framework 를 사용하는 개발자들이 많은 것 같아요. 저 또한 마찬가지구요.

혹시 우리는 이 Spring Framework 가 주변에서 많이 쓰이기 때문에 고민 없이 사용했던 것 아닐까요? 때로는 '너무나 당연하게 주변에 존재하는 것 들에 의문을 가져보는 게 어떨까?' 라고 생각하는 편이에요. 우리는 Spring Framework 가 어떻게 탄생했는 지, 어떤 이유 때문에 탄생했는 지 알고 쓰고 있을까요? 누군가는 그러한 정보들이 크게 중요하지 않다고 생각할 수 있겠지만, 제 생각은 조금 달라요. 기술에 대한 배경지식은 많이 알 수록 기술의 이해에 도움이 된다고 생각하는 편이에요.

저와 같은 생각을 가지고 계신 분이 있다면, 이번 포스팅이 상당히 도움이 될 지도 모르겠어요. 이번 시리즈는 '알고쓰자 Spring' 입니다. Spring 에 대해 이미 잘 알고 계신 분들도 계시겠지만, 때로는 우리가 너무 당연해서 잊고 있던 것들에 대해 정리 해 보려고 해요.

시리즈 첫 번째 포스팅의 주제는 Spring 이 어떻게 탄생하게 되었는가? 입니다. 재밌게 읽어주세요! 😀

Spring 이전의 웹 개발

EJB 라고 들어보신 적 있나요? EJB(Enterprise Java Bean)Sun Microsystems 가 제창한 규약약입니다. Spring 이전에 웹 개발에서 많이 사용 되었죠. 현재는 Jakarta Enterprise Beans 라는 이름으로 불리죠. 사실 Spring 의 역사에 대해 이야기를 하려고 자료를 찾다보면, 결국 Java (JDK) 의 역사에 대한 이야기로 일맥상통 하는 것 같아요. EJB 에 대한 이야기를 하기 전에 Java EE 에 대해 이야기 해 보아야 해요.

Java EE 는 아시는 분은 아시겠지만 Enterprise Edition 의 약자에요. 웹 프로그래밍에 필요한 여러가지 기능들(JSP, Servlet, JDBC, JNDI, JTA 등)을 포함하고 있는 플랫폼이죠. 엔터프라이즈 에디션이라는 말에 맞게 회사에서 서비스를 제공할 때 사용할 수 있는 서버사이드 애플리케이션 개발 도구들을 모아 둔 플랫폼입니다. 아무래도 서비스를 유저에게 제공하기 위한 플랫폼으로 여러가지가 있겠지만 웹을 지원해야 하고, 웹을 지원한다는 의미는 수 많은 웹 개발 도구들과 보안 기능들을 제공해야 한다는 의미죠.

EJB는 Java EE 에 포함되는 기능입니다. EJB는 분산 애플리케이션을 지원하는 컴포넌트 기반의 객체라고 합니다.

여기서 잠시! 분산 애플리케이션이란 무엇일까요? 여러 개의 독립적이고 작은 부분들로 나누어 동작하는 애플리케이션입니다. 마이크로서비스도 일종의 분산 애플리케이션이라고 할 수 있죠. 그렇다고 분산 애플리케이션 == 마이크로서비스라고 볼 수는 없습니다. 분산 애플리케이션 ⊃ 마이크로서비스가 좀 더 정확하죠.

EJB는 JBoss 와 같은 EJB Container 에서 서비스 됩니다. EJB 컴포넌트는 비즈니스 로직을 가지고 있고 재사용이 가능해요.

여기서 말하는 컨테이너라는 개념은 VM 에서 얘기하는 컨테이너와는 달라요. 비즈니스 로직을 탑재 한 컴포넌트 인스턴스를 의미해요. Database 처리, Transaction 처리 등의 로직을 감추고 있는 소프트웨어 부품을 컨테이너라고 불러요.

개발자들은 EJB를 통해 애플리케이션의 핵심 로직을 개발하고, 이를 재사용 하거나 분산 환경에서 실행하여, 다수의 서버에서 애플리케이션을 실행하도록 도울 수 있어요.

또한 앞서 말씀드렸던 것 처럼 서비스 제공에 필요한 트랜잭션 관리, 보안 기능, 스레드 관리 등에 크게 신경쓰지 않아도 괜찮아요. 비즈니스 로직에만 신경을 쓰고 나머지는 설정으로 해결할 수 있죠. EJB가 없었다면, 아마 이 모든 것들을 매번 수동으로 개발해야 했을거에요. 우리는 백엔드 애플리케이션 개발자다 보니 EJB 와 같은 기술이 없었다면, 애플리케이션 영역 외에 로우레벨의 구현 능력이 중요했을 거에요. 할 줄 알면 당연히 도움은 되겠지만, 사람마다 코딩 실력도 다르고 중요하게 생각하는 요소도 다르잖아요? 오히려 결과물의 신뢰도가 개발자의 퍼포먼스에 크게 의존한다는 문제점이 생기죠.

물론 때로는 바퀴를 발명 해 보는 게 공부에는 도움이 될 수 있지만, 당장 제품을 출시해야 하는 상황에서 까지 그럴 수는 없을 것이라 생각해요. 공부를 할 때는 밑 바닥 부터 구현 해 보는 게 큰 도움이 된다고 생각하지만, 회사 업무는 회사 업무니까요. 생산성이 더 중요하죠.

지금 까지의 이야기만 가지고 생각 해 보면 Spring 과 크게 다를 것 없어 보이지만 EJB는 다음과 같은 단점이 존재해요.

먼저 성능에 이슈가 있어요. 분산 환경을 지원하기 위해 객체를 직렬화 하는 과정에서 성능 이슈가 발생해요. 현실적으로 분산 트랜잭션을 써야 할 경우가 얼마나 있을까요? 해당 기능을 위해 무거운 JTA 기반의 글로벌 트랜잭션 관리 기능을 사용해야 했어요.

예를 들면, 특정 사이트에서 에러가 나서 다른 사이트를 포함한 이전의 모든 처리를 롤백해야 하는 상황이죠. MSA 등으로 프로젝트를 분리하고 뭔가 서비스가 대규모로 커지지 않는 이상 단순 웹 서비스 하나를 개발하기 위해 이런 기능을 사용 할 일은 잘 없을 것이라 생각해요.

다음으로 특정 환경, 특정 기술에 종속적인 코드를 작성해야 해요. EJB 스펙에 의존하는 코드를 개발해야 하죠. 즉, 개발자들은 순수 Java 코드를 작성한다는 느낌을 받기 어려워요.

import javax.ejb.Stateless;
import javax.ejb.EJB;

@Stateless
public class MyServiceEJB {

    public String process() {
        return "EJB Processing";
    }
}

public class MyApplication {

    @EJB
    private MyServiceEJB myServiceEJB;

    public void run() {
        System.out.println(myServiceEJB.process());
    }
}

비즈니스 로직을 작성하기 위해 EJB에 의존적인 어노테이션을 사용해야하고, 해당 인스턴스는 EJB 컨테이너가 없는 경우 동작하지 않아요. 특정 기술과 강결합 하는 코드는 객체지향 적인 설계 보단, 해당 기술에 종속적인 설계를 강제하도록 해요.

마지막으로 자동화 된 테스트가 매우 어렵거나 불가능해요. EJB 컨테이너의 외부에서 코드를 실행하는 게 어렵다보니, 테스트를 위해서는 반드시 컨테이너에 배포해야 했죠. 뭔가 단위테스트를 하고 싶어도 반드시 배포를 해야 했어요.

J2EE Design and Development

EJB 를 사용하는 것은 많은 장점을 가져요. 분산 트랜잭션 관리가 용이하고, 비즈니스 로직에 집중할 수 있는 코드를 작성할 수 있지만, 특정한 프레임워크에 종속적인 코드를 작성할 수 밖에 없죠. 만약 EJB의 설계 자체가 문제가 있다면, 우리의 서비스는 그 문제점도 함께 안고 갈 수 밖에 없어요.

하지만 우리는 Java를 사용하고 있죠. Kotlin 을 사용하고 있다고 해도 마찬가지죠. 프로그래밍 언어의 기본 기술에 충실하고 외부 기술에 종속성을 최대한 줄인 비즈니스 로직 코드를 작성하게 되면, 만약 다른 기술로 전환 하게 된다고 해도 비즈니스 로직은 영향을 받지 않아요.

Rod Johnson (로드 존슨) 의 저서 J2EE Design and Development 에서 POJO 형태의 예제코드를 공개했고, 해당 코드를 통해 기존 EJB의 문제점을 지적함과 동시에 BeanFactory, ApplicationContext, POJO, Inversion of Control, Dependency Injection 등의 예시 코드를 공개했어요.

Plain Old Java Object (POJO) 는 특정한 기술에 종속적이지 않은 순수 Java 객체를 의미해요. 정말 말 그대로 Java 만 가지고 작성 한 클래스는 POJO 라고 할 수 있어요.

이후 Juergen Hoeller(유겐 휠러), Yann Caroff(얀 카로프)가 Rod Johnson 에게 오픈소스 프로젝트를 제안하게 되었고 Spring 프로젝트가 시작되었어요.

Spring 프로젝트의 시작

Spring 의 뜻은 다들 잘 아시다시피 봄이에요. J2EE(EJB) 의 겨울을 넘어 새로운 봄의 시작이라는 의미로 이름 붙혀졌죠.

Spring 은 2003 년 6월 최초로 아파치 2.0 라이센스로 공개되었어요. 주요 버전 이력은 다음과 같아요.

  • 1.0 : 2004년 3월 : XML
  • 2.0 : 2006년 10월 : XML 편의기능 지원
  • 3.0 : 2009년 12월 : Java 코드를 통한 Config
  • 4.0 : 2013년 12월 : Java 8 지원
  • 5.0 : 2017년 9월 : Spring Framework 5.0, Spring Boot 2.0 출시, Reactive Programming 지원
  • 6.0 : 2022년 11월 : Spring Framework 6.0, Spring Boot 3.0 출시

Spring 은 EJB 를 사용하지 않고도 객체 간 의존성 해결이 가능한 컨테이너를 개발하자는 의견에서 시작했어요. 또한 POJO 를 이용해 EJB의 기능은 유지하되, 기술에 의존하는 복잡성을 제거했죠.

앞서 말씀드렸듯이 POJO는 순수 Java 객체잖아요? 우리가 프레임워크를 통해 개발할 때 import 하는 라이브러리 코드들을 살펴보면 상위로 갈 수록 순수 POJO 로만 이루어져있다는 것을 알 수 있어요.

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}

당장 우리가 상당히 많이 import 하는 @Component 어노테이션 코드를 살펴보면, java.lang 패키지 외에 아무것도 사용하지 않고 있음을 알 수 있죠. POJO 위주의 프레임워크라는 것은 순수한 Java 기반의 프레임워크라는 의미이고, Java 언어의 특징을 최대한 살린 소프트웨어를 개발할 수 있다는 의미에요.

Java 의 특징을 살린다는 것의 의미

Java 의 특징을 살린다는 게 그렇게 중요한 의미를 가지고 있나요?

Java 를 처음 공부해보신 분도 계실거에요. Java 에 대해 이야기 해 보기 전에 객체지향에 대해 이야기 해 보아야 할 것 같아요.

객체지향 프로그래밍이 뭘까요? 사전적인 정의는 ‘프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다.' 입니다.

그럼 객체는 또 뭘까요? 이 또한 사전적 정의 부터 말씀 드리자면 **'클래스 라고 부르는 명세서에서 정의한 것을 토대로 메모리에 할당 된 것’**입니다. 프로그램에서 사용하는 데이터 또는 식별자 등에 의해 참조되는 공간을 의미해요. 우리가 흔히 처음 코딩을 배울 때 먼저 배우는 변수, 그리고 Computer Science 전공 시간에 배우는 자료구조 (데이터구조라고도 하죠), 함수 또는 메소드가 될 수 있어요.

여담이지만, 개인적으로는 함수라는 변역을 좋아하지 않습니다. function 이라는 단어 자체가 가진 의미를 오히려 고정시킨다고 생각해요.

만약 C(혹은 C++) 부터 프로그래밍을 공부 해보신 분은 절차지향 프로그래밍을 겪어 보셨을 거에요. 프로그램이 실행되는 데 필요한 명령어들을 나열하는 형태로 코딩했던 경험 있으실거에요. 물론 절차지향 프로그래밍에서도 객체라는 개념은 존재합니다. 절차지향 프로그래밍에서의 객체는 데이터나 명령 (함수) 를 포함할 수 있겠지만, 동시에 포함하지는 않아요.

객체지향 프로그래밍에서의 객체는 클래스 라는 명세를 통해 메모리에 할당 된 것이죠. 흔히 인스턴스라고 많이 부릅니다만 이번 포스팅에서는 객체로 통일 하겠습니다. 객체는 데이터와, 데이터를 다루는 명령들의 조합을 함께 포함할 수 있습니다. 객체는 메시지를 받을 수 있고, 데이터를 처리할 수 있으며, 메시지를 다른 객체로 전달할 수도 있어요.

절차지향과 객체지향의 성격을 한 눈에 볼 수 있는 이미지를 가져왔습니다. 이해하시는 데 도움이 되실까요?

절차지향에서는 하나의 로직에 대한 흐름을 중심으로 프로그램이 작성되죠. 우리가 돈을 넣고 자판기에서 물건을 사는 모든 과정 전체에 집중한다면, 객체지향 프로그래밍에서는 고객, 자판기 두 객체가 서로 상호작용 하는 과정, 그리고 객체의 행위에 집중해요.

Java 이전에도 객체지향 프로그래밍 언어가 존재했지만, Java는 훨씬 편리하게 객체지향 프로그래밍을 할 수 있도록 도와줘요. 즉, Java의 특징을 살린 프레임워크라는 건 객체지향 프로그래밍에 이점을 가졌다고 볼 수 있죠.

객체지향 프로그래밍?

객체지향 프로그래밍이 중요한가요? 어떤 이점을 가지고 있나요?

앞서 말씀드렸듯 객체지향 프로그래밍은 객체간의 상호작용에 집중해요. 이 말은 객체 각각이 독립적으로 간주된다는 의미죠.

우리가 클래스를 통해 객체를 명세하잖아요? 이 말은 클래스 하나하나 또한 독립적으로 존재할 수 있다는 의미고, 이 특징을 활용해서 우리는 모듈화를 할 수 있어요. 마치 우리가 건물을 지을 때 건물의 청사진을 보고, 필요한 부품들을 가져와서 공사를 하는 것과 비슷해요. 각 부품들의 청사진이 클래스, 실제 부품은 객체라고 생각해보면 어떨까요?

이런 특징들은 다수의 개발자들이 대규모 프로젝트를 하는 경우 유리하게 작용합니다. 개발자 마다 담당한 기능이 다양할 수 있잖아요. 객체지향 프로그래밍은 이런 상황에서 개발자 각자의 영역을 침범하지 않도록 도와주죠.

하지만 이게 다가 아니에요. 객체지향 프로그래밍이란 개념 자체는 심플한데, 사람마다 생각하는 좋은 객체의 기준이 다르잖아요. 누군가는 객체 하나가 다양한 책임을 가지는 게 좋다고 생각 할 수 있죠. 이런 다양한 고민들이 엮이다보면 아무리 객체지향 프로그래밍을 도입했다고 해도 코드의 복잡도를 낮추는 데 도움이 되지 않을 수 있어요.

그래서 Robert Martin(로버트 마틴) 의 ‘Clean Code’, Michael Feathers (마이클 패더스) 의 객체지향 설계 5 원칙 (SOLID 원칙) 등 객체지향 프로그래밍에 도움이 되는 여러가지 연구 결과물이 나오기 시작했어요. 아마 들어보신 분도 계실거에요.

간단하게 SOLID 원칙을 정리해보면 아래와 같아요.

  • S : SRP (Single Responsibility Principle) 단일 책임 원칙
    • 한 클래스는 하나의 책임만 가져야 한다.
  • O : OCP (Open / Closed Principle) 개방 폐쇄 원칙
    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • L : LSP (Liskov Substitution Principle) 리스코프 치환 원칙
    • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • I : ISP (Interface Segregation Principle) 인터페이스 분리 원칙
    • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • D : DIP (Dependency Inversion Principle) 의존관계 역전 원칙
    • 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

Spring, Java, 객체지향

문제는 Java 자체가 오래 된 언어기도 하고 이런 원칙들은 프로그래머들이 객체지향 프로그래밍을 실제로 적용 해 보면서 나왔던 연구 결과물이에요. Java를 처음 설계했을 때 부터 이런 원칙이 존재했던 건 아니에요.

실제로 Java 는 OCP, DIP 를 지키기 어려워요. 다형성을 지키기 위해 인터페이스 타입을 가진 객체를 생성한다고 생각 해보면, 결국 인터페이스를 데이터타입으로 갖는 객체는 클래스가 변화할 때 영향을 받죠.

Shape shape = new Circle(); // 인터페이스가 구체적인 클래스에 의존

Shape 인터페이스를 타입으로 갖는 상황에서도 결국 객체를 생성하려면 구체적인 클래스에 의존해야 해요. 클래스가 변화하면 인터페이스 데이터타입을 갖는 객체도 변화하므로 DIP 를 위반하게 되죠. 만약 Circle 클래스가 문제가 생겨서 새로운 클래스 CircleV2 로 변경한다고 하면 위의 코드 또한 수정해야 하므로 OCP를 위반하게 되어요.

Spring 에서는 DI, IoC 등을 활용해서 이런 원칙들을 위배하지 않도록 도와줘요. 즉 Spring 을 쓴다는 건 객체지향 프로그래밍의 패러다임을 지키며, Java의 특징을 살릴 수 있되, Java가 가지는 문제점을 개선할 수 있다는 이점을 가져요.

이런 이점은 Spring 프레임워크가 단순히 하나로 정의할 수 없는 다양한 모듈을 가지는 데 크게 기여했어요. 우리가 Spring 애플리케이션을 개발할려고 보면 정말 많은 Spring 관련 라이브러리들이 존재하는 것을 알 수 있죠. Spring Boot 를 쓰시는 경우엔 수 많은 Starter 들을 볼 수 있으니까요.

Outro

지금까지 Spring 이 어떻게 시작했는 지, Spring 이전에 비해 어떤 점이 개선되었는 지, Spring 은 어떤 설계 철학을 가지고 있는 지를 알아보았어요. 재밌게 읽으셨나요?

다음 포스팅은 Spring 의 변천사입니다. Spring 이 어떻게 변화해서 지금의 6버전 까지 업데이트 되었는 지 알고 써 보도록 해요!

Reference

https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%94%84%EB%A7%81_%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC
https://jhyonhyon.tistory.com/28
https://wikim.tistory.com/12
https://hoon93.tistory.com/56
https://gaebalsogi.tistory.com/37
https://ko.wikipedia.org/wiki/%EA%B0%9D%EC%B2%B4*%EC%A7%80%ED%96%A5*%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D